package com.gmail.dengtao.joe.commons.util;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;

/**
 * <p>A suite of utilities surrounding the use of the
 * {@link java.util.Calendar} and {@link java.util.Date} object.</p>
 * 
 * <p>DateUtils contains a lot of common methods considering manipulations
 * of Dates or Calendars. Some methods require some extra explanation.
 * The truncate, ceiling and round methods could be considered the Math.floor(),
 * Math.ceil() or Math.round versions for dates
 * This way date-fields will be ignored in bottom-up order.
 * As a complement to these methods we've introduced some fragment-methods.
 * With these methods the Date-fields will be ignored in top-down order.
 * Since a date without a year is not a valid date, you have to decide in what
 * kind of date-field you want your result, for instance milliseconds or days.
 * </p>
 * 
 * @author <a href="mailto:joe.dengtao@gmail.com">DengTao</a>
 * @version 1.0
 * @since 1.0
 */
public class DateUtils {

    /** Number of milliseconds in a standard second. */
    public static final long MILLIS_PER_SECOND = 1000;
    /** Number of milliseconds in a standard minute. */
    public static final long MILLIS_PER_MINUTE = 60 * MILLIS_PER_SECOND;
    /** Number of milliseconds in a standard hour. */
    public static final long MILLIS_PER_HOUR = 60 * MILLIS_PER_MINUTE;
    /** Number of milliseconds in a standard day.  */
    public static final long MILLIS_PER_DAY = 24 * MILLIS_PER_HOUR;
    
    /** This is half a month, so this represents whether a date is in the top or bottom half of the month. */
    public static final int SEMI_MONTH = 1001;
    /** A week range, starting on Sunday. */
    public static final int RANGE_WEEK_SUNDAY = 1;
    /** A week range, starting on Monday. */
    public static final int RANGE_WEEK_MONDAY = 2;
    /** A week range, starting on the day focused. */
    public static final int RANGE_WEEK_RELATIVE = 3;
    /** A week range, centered around the day focused. */
    public static final int RANGE_WEEK_CENTER = 4;
    /** A month range, the week starting on Sunday. */
    public static final int RANGE_MONTH_SUNDAY = 5;
    /** A month range, the week starting on Monday. */
    public static final int RANGE_MONTH_MONDAY = 6;
    /** The default time format  */
    public static final String DEFAULT_TIME_FORMAT = "yyyyMMddHHmmss";
    /** The current value of the default locale for this instance of the Java Virtual Machine. */
    public static final Locale DEFAULT_LOCALE = Locale.getDefault();
    
    /**
     * Parses text from the beginning of the given string to produce a date. 
     * The method may not use the entire text of the given string. 
     * @param date
     * @param format the pattern describing the date and time format
     * @param locale the locale whose date format symbols should be used
     * @return A Date parsed from the string.
     * @throws ParseException
     * @since 1.0
     */
    public static Date parse(String date, String format, Locale locale)
			throws ParseException {
		SimpleDateFormat dateFormat = new SimpleDateFormat(format == null ? DEFAULT_TIME_FORMAT : format, locale == null ? DEFAULT_LOCALE : locale);
		Date nowTime = dateFormat.parse(date);
		return nowTime;
	}
    
    /**
     * Parses text from the beginning of the given string to produce a date. 
     * The method may not use the entire text of the given string. 
     * @param date
     * @param format the pattern describing the date and time format
     * @return A Date parsed from the string.
     * @throws ParseException
     * @since 1.0
     */
    public static Date parse(String date, String format)
    		throws ParseException {
    	return parse(date, format, DEFAULT_LOCALE);
    }
    
    /**
     * Format the <code>date</code> with the specified <code>format</code>.
     * @param date the date
     * @param format date format eg:yyyyMMddHHmmss
     * @return data after the specified format
     * @since 1.0
     */
    public static String format(Date date, String format) {
		return format(date, format, DEFAULT_LOCALE);
	}
    
    /**
     * Format the <code>date</code> with the specified <code>format</code>.
     * @param date the date
     * @param format date format eg:yyyyMMddHHmmss
     * @param locale the locale whose date format symbols should be used
     * @return data after the specified format
     * @since 1.0
     */
    public static String format(Date date, String format, Locale locale) {
    	SimpleDateFormat dateFormat = new SimpleDateFormat(format == null ? DEFAULT_TIME_FORMAT : format, locale == null ? DEFAULT_LOCALE : locale);
    	String nowTime = dateFormat.format(date);
    	return nowTime;
    }
    
    /**
     * Get the current time with the specified format;
     * @param format date format eg:yyyyMMddHHmmss
     * @return current time after the specified format
     * @since 1.0
     */
    public static String getNow(String format) {
		return getNow(format, DEFAULT_LOCALE);
	}
    
    /**
     * Get the current time with the specified format.
     * @param format time format eg:yyyyMMddHHmmss
     * @param locale the locale whose date format symbols should be used
     * @return current time with the specified format
     * @since 1.0
     */
    public static String getNow(String format, Locale locale) {
    	SimpleDateFormat dateFormat = new SimpleDateFormat(format == null ? DEFAULT_TIME_FORMAT : format, locale == null ? DEFAULT_LOCALE : locale);
    	String nowTime = dateFormat.format(new Date());
    	return nowTime;
    }

    //-----------------------------------------------------------------------
    /**
     * <p>Checks if two date objects are on the same day ignoring time.</p>
     *
     * <p>28 Mar 2002 13:45 and 28 Mar 2002 06:01 would return true.
     * 28 Mar 2002 13:45 and 12 Mar 2002 13:45 would return false.
     * </p>
     * 
     * @param date1  the first date, not altered, not null
     * @param date2  the second date, not altered, not null
     * @return true if they represent the same day
     * @throws IllegalArgumentException if either date is <code>null</code>
     * @since 1.0
     */
    public static boolean isSameDay(Date date1, Date date2) {
        if (date1 == null || date2 == null) {
            throw new IllegalArgumentException("The date must not be null");
        }
        Calendar cal1 = Calendar.getInstance();
        cal1.setTime(date1);
        Calendar cal2 = Calendar.getInstance();
        cal2.setTime(date2);
        return isSameDay(cal1, cal2);
    }

    /**
     * <p>Checks if two calendar objects are on the same day ignoring time.</p>
     *
     * <p>28 Mar 2002 13:45 and 28 Mar 2002 06:01 would return true.
     * 28 Mar 2002 13:45 and 12 Mar 2002 13:45 would return false.
     * </p>
     * 
     * @param cal1  the first calendar, not altered, not null
     * @param cal2  the second calendar, not altered, not null
     * @return true if they represent the same day
     * @throws IllegalArgumentException if either calendar is <code>null</code>
     * @since 1.0
     */
    public static boolean isSameDay(Calendar cal1, Calendar cal2) {
        if (cal1 == null || cal2 == null) {
            throw new IllegalArgumentException("The date must not be null");
        }
        return (cal1.get(Calendar.ERA) == cal2.get(Calendar.ERA) &&
                cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) &&
                cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR));
    }

    //-----------------------------------------------------------------------
    /**
     * <p>Checks if two calendar objects represent the same local time.</p>
     *
     * <p>This method compares the values of the fields of the two objects.
     * In addition, both calendars must be the same of the same type.</p>
     * 
     * @param cal1  the first calendar, not altered, not null
     * @param cal2  the second calendar, not altered, not null
     * @return true if they represent the same millisecond instant
     * @throws IllegalArgumentException if either date is <code>null</code>
     * @since 1.0
     */
    public static boolean isSameLocalTime(Calendar cal1, Calendar cal2) {
        if (cal1 == null || cal2 == null) {
            throw new IllegalArgumentException("The date must not be null");
        }
        return (cal1.get(Calendar.MILLISECOND) == cal2.get(Calendar.MILLISECOND) &&
                cal1.get(Calendar.SECOND) == cal2.get(Calendar.SECOND) &&
                cal1.get(Calendar.MINUTE) == cal2.get(Calendar.MINUTE) &&
                cal1.get(Calendar.HOUR_OF_DAY) == cal2.get(Calendar.HOUR_OF_DAY) &&
                cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR) &&
                cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) &&
                cal1.get(Calendar.ERA) == cal2.get(Calendar.ERA) &&
                cal1.getClass() == cal2.getClass());
    }

    //-----------------------------------------------------------------------
    /**
     * Adds a number of years to a date returning a new object.
     * The original {@code Date} is unchanged.
     *
     * @param date  the date, not null
     * @param amount  the amount to add, may be negative
     * @return the new {@code Date} with the amount added
     * @throws IllegalArgumentException if the date is null
     * @since 1.0
     */
    public static Date addYears(Date date, int amount) {
        return add(date, Calendar.YEAR, amount);
    }

    //-----------------------------------------------------------------------
    /**
     * Adds a number of months to a date returning a new object.
     * The original {@code Date} is unchanged.
     *
     * @param date  the date, not null
     * @param amount  the amount to add, may be negative
     * @return the new {@code Date} with the amount added
     * @throws IllegalArgumentException if the date is null
     * @since 1.0
     */
    public static Date addMonths(Date date, int amount) {
        return add(date, Calendar.MONTH, amount);
    }

    //-----------------------------------------------------------------------
    /**
     * Adds a number of weeks to a date returning a new object.
     * The original {@code Date} is unchanged.
     *
     * @param date  the date, not null
     * @param amount  the amount to add, may be negative
     * @return the new {@code Date} with the amount added
     * @throws IllegalArgumentException if the date is null
     * @since 1.0
     */
    public static Date addWeeks(Date date, int amount) {
        return add(date, Calendar.WEEK_OF_YEAR, amount);
    }

    //-----------------------------------------------------------------------
    /**
     * Adds a number of days to a date returning a new object.
     * The original {@code Date} is unchanged.
     *
     * @param date  the date, not null
     * @param amount  the amount to add, may be negative
     * @return the new {@code Date} with the amount added
     * @throws IllegalArgumentException if the date is null
     * @since 1.0
     */
    public static Date addDays(Date date, int amount) {
        return add(date, Calendar.DAY_OF_MONTH, amount);
    }

    //-----------------------------------------------------------------------
    /**
     * Adds a number of hours to a date returning a new object.
     * The original {@code Date} is unchanged.
     *
     * @param date  the date, not null
     * @param amount  the amount to add, may be negative
     * @return the new {@code Date} with the amount added
     * @throws IllegalArgumentException if the date is null
     * @since 1.0
     */
    public static Date addHours(Date date, int amount) {
        return add(date, Calendar.HOUR_OF_DAY, amount);
    }

    //-----------------------------------------------------------------------
    /**
     * Adds a number of minutes to a date returning a new object.
     * The original {@code Date} is unchanged.
     *
     * @param date  the date, not null
     * @param amount  the amount to add, may be negative
     * @return the new {@code Date} with the amount added
     * @throws IllegalArgumentException if the date is null
     * @since 1.0
     */
    public static Date addMinutes(Date date, int amount) {
        return add(date, Calendar.MINUTE, amount);
    }

    //-----------------------------------------------------------------------
    /**
     * Adds a number of seconds to a date returning a new object.
     * The original {@code Date} is unchanged.
     *
     * @param date  the date, not null
     * @param amount  the amount to add, may be negative
     * @return the new {@code Date} with the amount added
     * @throws IllegalArgumentException if the date is null
     * @since 1.0
     */
    public static Date addSeconds(Date date, int amount) {
        return add(date, Calendar.SECOND, amount);
    }

    //-----------------------------------------------------------------------
    /**
     * Adds a number of milliseconds to a date returning a new object.
     * The original {@code Date} is unchanged.
     *
     * @param date  the date, not null
     * @param amount  the amount to add, may be negative
     * @return the new {@code Date} with the amount added
     * @throws IllegalArgumentException if the date is null
     * @since 1.0
     */
    public static Date addMilliseconds(Date date, int amount) {
        return add(date, Calendar.MILLISECOND, amount);
    }

    //-----------------------------------------------------------------------
    /**
     * Adds to a date returning a new object.
     * The original {@code Date} is unchanged.
     *
     * @param date  the date, not null
     * @param calendarField  the calendar field to add to
     * @param amount  the amount to add, may be negative
     * @return the new {@code Date} with the amount added
     * @throws IllegalArgumentException if the date is null
     * @since 1.0
     */
    private static Date add(Date date, int calendarField, int amount) {
        if (date == null) {
            throw new IllegalArgumentException("The date must not be null");
        }
        Calendar c = Calendar.getInstance();
        c.setTime(date);
        c.add(calendarField, amount);
        return c.getTime();
    }
    
    //-----------------------------------------------------------------------
    /**
     * Sets the years field to a date returning a new object.
     * The original {@code Date} is unchanged.
     *
     * @param date  the date, not null
     * @param amount the amount to set
     * @return a new {@code Date} set with the specified value
     * @throws IllegalArgumentException if the date is null
     * @since 1.0
     */
    public static Date setYears(Date date, int amount) {
        return set(date, Calendar.YEAR, amount);
    }

    //-----------------------------------------------------------------------
    /**
     * Sets the months field to a date returning a new object.
     * The original {@code Date} is unchanged.
     *
     * @param date  the date, not null
     * @param amount the amount to set
     * @return a new {@code Date} set with the specified value
     * @throws IllegalArgumentException if the date is null
     * @since 1.0
     */
    public static Date setMonths(Date date, int amount) {
        return set(date, Calendar.MONTH, amount);
    }

    //-----------------------------------------------------------------------
    /**
     * Sets the day of month field to a date returning a new object.
     * The original {@code Date} is unchanged.
     *
     * @param date  the date, not null
     * @param amount the amount to set
     * @return a new {@code Date} set with the specified value
     * @throws IllegalArgumentException if the date is null
     * @since 1.0
     */
    public static Date setDays(Date date, int amount) {
        return set(date, Calendar.DAY_OF_MONTH, amount);
    }

    //-----------------------------------------------------------------------
    /**
     * Sets the hours field to a date returning a new object.  Hours range 
     * from  0-23.
     * The original {@code Date} is unchanged.
     *
     * @param date  the date, not null
     * @param amount the amount to set
     * @return a new {@code Date} set with the specified value
     * @throws IllegalArgumentException if the date is null
     * @since 1.0
     */
    public static Date setHours(Date date, int amount) {
        return set(date, Calendar.HOUR_OF_DAY, amount);
    }

    //-----------------------------------------------------------------------
    /**
     * Sets the minute field to a date returning a new object.
     * The original {@code Date} is unchanged.
     *
     * @param date  the date, not null
     * @param amount the amount to set
     * @return a new {@code Date} set with the specified value
     * @throws IllegalArgumentException if the date is null
     * @since 1.0
     */
    public static Date setMinutes(Date date, int amount) {
        return set(date, Calendar.MINUTE, amount);
    }
    
    //-----------------------------------------------------------------------
    /**
     * Sets the seconds field to a date returning a new object.
     * The original {@code Date} is unchanged.
     *
     * @param date  the date, not null
     * @param amount the amount to set
     * @return a new {@code Date} set with the specified value
     * @throws IllegalArgumentException if the date is null
     * @since 1.0
     */
    public static Date setSeconds(Date date, int amount) {
        return set(date, Calendar.SECOND, amount);
    }

    //-----------------------------------------------------------------------
    /**
     * Sets the miliseconds field to a date returning a new object.
     * The original {@code Date} is unchanged.
     *
     * @param date  the date, not null
     * @param amount the amount to set
     * @return a new {@code Date} set with the specified value
     * @throws IllegalArgumentException if the date is null
     * @since 1.0
     */
    public static Date setMilliseconds(Date date, int amount) {
        return set(date, Calendar.MILLISECOND, amount);
    } 
    
    //-----------------------------------------------------------------------
    /**
     * Sets the specified field to a date returning a new object.  
     * This does not use a lenient calendar.
     * The original {@code Date} is unchanged.
     *
     * @param date  the date, not null
     * @param calendarField  the {@code Calendar} field to set the amount to
     * @param amount the amount to set
     * @return a new {@code Date} set with the specified value
     * @throws IllegalArgumentException if the date is null
     * @since 1.0
     */
    private static Date set(Date date, int calendarField, int amount) {
        if (date == null) {
            throw new IllegalArgumentException("The date must not be null");
        }
        // getInstance() returns a new object, so this method is thread safe.
        Calendar c = Calendar.getInstance();
        c.setLenient(false);
        c.setTime(date);
        c.set(calendarField, amount);
        return c.getTime();
    }   

    //-----------------------------------------------------------------------
    /**
     * Convert a {@code Date} into a {@code Calendar}. 
     * 
     * @param date the date to convert to a Calendar
     * @return the created Calendar
     * @throws NullPointerException if null is passed in
     * @since 1.0
     */
    public static Calendar toCalendar(Date date) {
        Calendar c = Calendar.getInstance();
        c.setTime(date);
        return c;
    }
    
    /**
     * Check if date is between min and max.
     * @param date date to check. 
     * @param min 
     * @param max
     * @return true while date >= min && date <= max
     * @since 1.0
     */
    public static boolean isBetween(Date date, Date min, Date max) {
    	return date.compareTo(min) >= 0 && date.compareTo(max) <= 0;
    }
    
    /**
     * Check if date's hours is in period:min ~ max.
     * @param date date to check. 
     * @param min min hours in 24hours format
     * @param max max hours in 24hours format
     * @return true while date.getHours >= min && date.getHours < max
     * @since 1.0
     */
    public static boolean isBetween(Date date, int min, int max) {
    	Calendar cal = Calendar.getInstance();
    	cal.setTime(date);
    	int hours = cal.get(Calendar.HOUR_OF_DAY);
    	return hours >= min && hours < max;
    }

}
